package cipher.io;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;

import cipher.util.CipherConst;
import cipher.util.IFCrypt;
import cipher.util.Util;



public class CipherFileChannel extends FileChannel {

	private FileChannel channel;
	private static final int TRANSFER_SIZE = CipherConst.kBufferSize;
	private IFCrypt cipher = null;
	private long vfp = 0;
	private long vlen = 0;
	private CipherFile file;
	private static byte dummy[] = new byte[0];
	private byte _tmpR[] = new byte[CipherConst.enc_kBufferSize];
	private byte dbuf[]=null;
	private int dlen;
	private int dpos;
	private ByteBuffer wbuf= ByteBuffer.allocateDirect(CipherConst.kBufferSize);
	private ByteBuffer _tmpBB = ByteBuffer.allocateDirect(CipherConst.enc_kBufferSize);
	private RandomAccessFile access = null;
	private boolean writeable;
	private boolean readable;
	
	public CipherFileChannel(FileChannel fileChannel, CipherFile cipherfile,boolean writeable,boolean readable) throws FileNotFoundException {
		this.channel = fileChannel;
		this.file = cipherfile;
		this.vlen = cipherfile.length();
		this.cipher = cipherfile.getCipher();
		this.access = new RandomAccessFile(file.getPhysicalFile(), "r");
		this.writeable = writeable;
		this.readable = readable;
	}

	@Override
	public void force(boolean metaData) throws IOException {
		this.channel.force(metaData);
	}

	@Override
	public FileLock lock(long position, long size, boolean shared)	throws IOException {
		long b = CipherConst.getDecodedLastBlockPosition(position);
		long s = CipherConst.getDecodedLastBlockPosition(size);
		if (s < size) {
			s += CipherConst.kBufferSize;
		}
		return this.channel.lock(CipherConst.convert2encodedBlockPosition(b),CipherConst.convert2encodedBlockPosition(s), shared);
	}

	@Override
	public MappedByteBuffer map(MapMode mode, long position, long size)	throws IOException {
		long b = CipherConst.getDecodedLastBlockPosition(position);
		long s = CipherConst.getDecodedLastBlockPosition(size);
		if (s < size) {
			s += CipherConst.kBufferSize;
		}
		return this.channel.map(mode, b, s);
	}

	@Override
	public long position() throws IOException {
		return vfp;
	}

	@Override
	public FileChannel position(long newPosition) throws IOException {
		this.vfp = newPosition;
		return this;
	}
	private int read_1(ByteBuffer vbuf, int offset, int len) throws IOException {
		long rp = vfp+len;
		if(vfp==vlen) return -1;
		if(rp > vlen){
			rp = vlen;
			len = (int)(vlen-vfp);
		}
		int orgLen = len;
		if (dpos > 0 && dpos < dlen) {
			int r = (dlen - dpos);
			int rest = r > len ? len : r;
			vbuf.put(dbuf,dpos,rest);
			len -= rest;
			dpos += rest;
			vfp += rest;
			if (len == 0)
				return orgLen;
		}

		long sp = CipherConst.getDecodedLastBlockPosition(vfp);  
		long esp = CipherConst.convert2encodedBlockPosition(sp);
		channel.position(esp);
		_tmpBB.clear();
		int read = channel.read(_tmpBB);
		_tmpBB.flip();
		_tmpBB.get(_tmpR, 0, read);
		dbuf = cipher.decrypt(_tmpR, 0, read);
		int bpos=0;
		if(dbuf!=null){
			dlen = dbuf.length;
			int of = (int)(vfp-sp);
			int rest = dlen-of;
			int clen = rest > len ? len : rest;
			if(vbuf!=null){
				vbuf.position(offset);
				vbuf.put(dbuf, of, clen);
				dpos=of+clen;
			}else{
				dpos=clen;
			}
			len -=clen;
			bpos +=clen;
			vfp+= clen;
		}
		while(len>0){
			_tmpBB.clear();
			read = channel.read(_tmpBB);
			_tmpBB.flip();
			_tmpBB.get(_tmpR, 0, read);
			dbuf = cipher.decrypt(_tmpR, 0, read);
			if( dbuf !=null){
				dlen = dbuf.length;
				int rest = dlen > len ? len : dlen;
				if(vbuf!=null){
					vbuf.position(offset+bpos);
					vbuf.put(dbuf, 0, rest);
					dpos=rest;
				}
				len -=rest;
				bpos+=rest;
				vfp+= rest;
			}
		}
		return orgLen;
	}

	@Override
	public int read(ByteBuffer dst) throws IOException {
		if(!readable ) throw new IOException("no readable channel");
		return read_1(dst, 0, dst.capacity());
	}

	@Override
	public int read(ByteBuffer dst, long position) throws IOException {
		if(!readable ) throw new IOException("no readable channel");
		// TODO Auto-generated method stub
		vfp = position;
		return read_1(dst, 0, dst.capacity());
	}

	@Override
	public long read(ByteBuffer[] dsts, int offset, int length)
			throws IOException {
		if(!readable ) throw new IOException("no readable channel");
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public long size() throws IOException {
		return vlen;
	}

	@Override
	public long transferFrom(ReadableByteChannel src, long position, long count)
			throws IOException {
		if(src instanceof CipherFileChannel){
			int c = (int)Math.min(count, TRANSFER_SIZE);
	        ByteBuffer bb = Util.getTemporaryDirectBuffer(c);
	        long tw = 0;                    // Total bytes written
	        long pos = position;
	        try {
	            bb.clear();
	            while (tw < count) {
	                bb.limit((int)Math.min((count - tw), (long)TRANSFER_SIZE));
	                int nr = src.read(bb);
	                if (nr <= 0)
	                    break;
	                bb.flip();
	                int nw = write(bb, pos);
	                tw += nw;
	                if (nw != nr)
	                    break;
	                pos += nw;
	                bb.clear();
	            }    
	            return tw;
	        } catch (IOException x) {
	        	x.printStackTrace();
	            if (tw > 0) 
	                return tw;
	            throw x;
	        } finally {
	            Util.releaseTemporaryDirectBuffer(bb);
	        }    
		}else if(src instanceof FileChannel){
			FileChannel a = (FileChannel) src;
			return a.transferTo(position, count, this);
		}else return -1;

	}

	@Override
	public long transferTo(long position, long count, WritableByteChannel target)
			throws IOException {
		if(target instanceof CipherFileChannel){
			int icount = (int)Math.min(count, Integer.MAX_VALUE);
			int c = Math.min(icount, TRANSFER_SIZE);
			ByteBuffer bb = Util.getTemporaryDirectBuffer(c);
			long tw = 0; // Total bytes written
			long pos = position;
			try {
				//Util.erase(bb);
				bb.clear();
				while (tw < icount) {
					bb.limit(Math.min((int) (icount - tw), TRANSFER_SIZE));
					int nr = read(bb, pos);
					if (nr <= 0)
						break;
					bb.flip();
					// ## Bug: Will block writing target if this channel
					// ## is asynchronously closed
					int nw = target.write(bb);
					tw += nw;
					if (nw != nr)
						break;
					pos += nw;
					bb.clear();
				}
				return tw;
			} catch (IOException x) {
				x.printStackTrace();
				if (tw > 0)
					return tw;
				throw x;
			} finally {
				Util.releaseTemporaryDirectBuffer(bb);
			}
		}else if(target instanceof FileChannel){
			FileChannel a =(FileChannel )target;
			return a.transferFrom(this, position, count);
		}else{
			return -1;
		}
	}

	@Override
	public FileChannel truncate(long length) throws IOException {
		System.out.println("TRUC");
		if(!writeable ) throw new IOException("no writeable channel");
		long newEncLength = CipherConst.getEcodeFullValue(length);
		if (length < vlen) { // 파일 크기가 작아졌을경우
			long sp = CipherConst.getDecodedLastBlockPosition(length); // 변경될 크기의 마지막 블록
			long esp = CipherConst.convert2encodedBlockPosition(sp); // 마지막 블록의 물리적 위치
			int r=0;
			if(readable){
				channel.position(esp);
				_tmpBB.clear();
				r = channel.read(_tmpBB);
				_tmpBB.flip();
				_tmpBB.get(_tmpR, 0, r);
			}else{
				access.seek(esp);
				r = access.read(_tmpR);
			}
			channel.truncate(newEncLength); // 물리적 파일의 크기를 변경
			if (r > 0 && sp != length) { // 읽은 크기가 0 보다 크고 파일 크기가 마지막 블록 위치랄
											// 다를경우만
				cipher.decrypt(dummy, 0, 0);
				byte d[] = cipher.decrypt(_tmpR, 0, r);
				if (d != null) {
					channel.position(esp);
					cipher.encrypt(dummy, 0, 0);
					byte e[] = cipher.encrypt(d, 0, (int) (length - sp));
					if (e != null) {
						channel.write(ByteBuffer.wrap(e, 0, e.length));
					}
				}
			}
		} else if (length > vlen) { // 파일 크기가 커졌을 경우
			long vl = CipherConst.getDecodedLastBlockPosition(vlen);
			long evl = CipherConst.convert2encodedBlockPosition(vl);
			int r=0;
			if(readable){
				channel.position(evl);
				_tmpBB.clear();
				r = channel.read(_tmpBB);
				_tmpBB.flip();
				_tmpBB.get(_tmpR, 0, r);	
			}else{
				access.seek(evl);
				r=access.read(_tmpR);
			}
			channel.truncate(newEncLength);
			if (r > 0) {
				cipher.decrypt(dummy, 0, 0);
				byte d[] = cipher.decrypt(_tmpR, 0, r);
				if (d != null) {
					int _dlen = (int) (length - vl) > CipherConst.kBufferSize ? CipherConst.kBufferSize: (int) (length - vl);
					byte t[] = new byte[_dlen];
					System.arraycopy(d, 0, t, 0, d.length);
					channel.position(evl);
					cipher.encrypt(dummy, 0, 0);
					byte e[] = cipher.encrypt(t, 0, t.length);
					if (e != null) {
						channel.write(ByteBuffer.wrap(e, 0, e.length));
					}
				}
			}
		} else { // 변화가 없을 경우
			return this;
		}

		file.setEncLength(newEncLength);
		file.setLength(length);
		vlen = length;
		return this;
	}

	@Override
	public FileLock tryLock(long position, long size, boolean shared)
			throws IOException {
		long b = CipherConst.getDecodedLastBlockPosition(position);
		long s = CipherConst.getDecodedLastBlockPosition(size);
		if (s < size) {
			s += CipherConst.kBufferSize;
		}
		return this.channel.tryLock(b, s, shared);
	}
	private void write_1(ByteBuffer vbuf)		throws IOException {
		long rp = vfp+vbuf.remaining();
		long sp = CipherConst.getDecodedLastBlockPosition(vfp);
		long ep = CipherConst.getDecodedLastBlockPosition(rp);
		long esp = CipherConst.convert2encodedBlockPosition(sp);
		
		long bend = rp == ep ? rp : ep+CipherConst.kBufferSize;
		long bcopylen = vfp-sp;
		long acopylen = bend-rp;
		wbuf.clear();
		if( acopylen > 0 && ep+CipherConst.kBufferSize>=file.length()){
			if(rp < file.length()){
				acopylen = file.length()-rp;
			}else{
				acopylen = 0;
			}
		}
		if(bcopylen>0){
			int r=0;
			if(readable){
				channel.position(esp);
				_tmpBB.clear();
				r = channel.read(_tmpBB);
				_tmpBB.flip();
				_tmpBB.get(_tmpR, 0, r);
			}else{
				access.seek(esp);
				r = access.read(_tmpR);
			}
			cipher.decrypt(dummy, 0, 0);
			byte beforedec[] = cipher.decrypt(_tmpR, 0, r);
			if(beforedec!=null){
				wbuf.put(beforedec,0,(int)bcopylen);
			}
		}

		int bpos=0;
		while(vbuf.hasRemaining()){
			int rb = wbuf.remaining();
			int sb = vbuf.remaining();
			int clen  = rb > sb ? sb : rb;
			while(wbuf.hasRemaining()){
				if(vbuf.hasRemaining()){
					wbuf.put(vbuf.get());
				}else{
					break;
				}
			}
			bpos +=clen;
			if(!wbuf.hasRemaining()){
				wbuf.flip();
				wbuf.get(_tmpR,0,wbuf.remaining());
				byte p[] = cipher.encrypt(_tmpR, 0, wbuf.position());
				_tmpBB.clear();
				_tmpBB.put(p,0,p.length);
				_tmpBB.flip();
				_tmpBB.limit(p.length);
				channel.write(_tmpBB);
				
			}
		}
		long lastp = channel.position();
		if(acopylen>0){
			long eep = CipherConst.convert2encodedBlockPosition(ep);
			int r=0;
			if(readable){
				channel.position(eep);
				_tmpBB.clear();
				r = channel.read(_tmpBB);
				_tmpBB.flip();
				_tmpBB.put(_tmpR);
				_tmpBB.limit(r);
			}else{
				access.seek(eep);
				r = access.read(_tmpR);
			}
			byte afterdec []= cipher.decrypt(_tmpR, 0, r);
			if(afterdec !=null){
				wbuf.put(afterdec, (int)(rp-ep), (int)acopylen);
			}
		}
		if(wbuf.hasRemaining()){
			channel.position(lastp);
			int wp = wbuf.position();
			wbuf.flip();
			wbuf.get(_tmpR,0,wp);
			byte p[] = cipher.encrypt(_tmpR, 0, wbuf.position());
			wbuf.clear();
			wbuf.put(p,0,p.length);
			wbuf.flip();
			wbuf.limit(p.length);
			channel.write(wbuf);
		}
		vfp +=bpos;
		if(vfp > vlen) vlen = vfp;
		file.setLength(vlen);
		file.setEncLength(CipherConst.getEcodeFullValue(vlen));
	}

	@Override
	public int write(ByteBuffer src) throws IOException {
		if(!writeable ) throw new IOException("no writeable channel");
		int s = src.remaining();
		write_1(src);
		return s;
	}

	@Override
	public int write(ByteBuffer src, long position) throws IOException {
		System.out.println("k2");
		if(!writeable ) throw new IOException("no writeable channel");
		 vfp = position;
		int s = src.remaining();
		write_1(src);
		return s;
	}

	@Override
	public long write(ByteBuffer[] srcs, int offset, int length)	throws IOException {
		System.out.println("k3");
		if(!writeable ) throw new IOException("no writeable channel");
		for (int i = 0; i < length; i++) {
			if (i == 0) {
				srcs[i].position(offset);
			}
			write_1(srcs[i]);
		}
		return 0;
	}

	@Override
	protected void implCloseChannel() throws IOException {
		channel.close();
		access.close();
	}
	
}
